/**
 * 
 */
package matheus.tp01cg.preenchimento;

import java.util.Stack;

import android.graphics.Bitmap;
import android.os.Handler;
import android.util.Pair;

/**
 * Implementacao dos algoritmos Boundary-Fill e Flood-Fill, utlizando
 * conectivada 4.
 * 
 * Ambos os algoritmos foram implementados de forma iterativa, utilizando pilha.
 * A implementacao com chamadas recursivas resultou em estouro da pilha de
 * execucao (StackOverflow) mesmo para preechimento de pequenas areas
 * (provavelmente pelo fato dos dispositivos c/ SO Android usualmente disporem
 * de pouca memoria).
 * 
 * @author matheus
 * 
 */
abstract public class Preechimento {
	/**
	 * Tamanho maximo para as pilhas utilizadas nos algoritmos. Ao ultrapassar
	 * esse valor, o algoritmo para de preencher (mesmo estando incompleto)
	 */
	public static final int TAMANHO_MAXIMO_PILHA = 25000;

	private static volatile boolean preenchimentoEmAndamento = false;

	/**
	 * Sinaliza se ha preenchimento sendo executado
	 */
	public static boolean emAndamento() {
		return preenchimentoEmAndamento;
	}

	/**
	 * Interface para chamada do preenchimento via Flood-Fill. O preenchimento
	 * eh realizado em outra Thread (@see FloodFillThread). A implementacao do
	 * algoritimo encontra-se no metodo FloodFillThread.run()
	 * 
	 * @param bitmap
	 * @param x
	 * @param y
	 * @param corPreenchimento
	 * @param corAntiga
	 */
	public static void floodFill(Bitmap bitmap, int x, int y,
			int corPreenchimento, int corAntiga) {
		if (!preenchimentoEmAndamento) {
			FloodFillThread floodThread = new FloodFillThread(bitmap, x, y,
					corPreenchimento, corAntiga);
			floodThread.start();
		}
	}

	private static OnPreechimentoPronto onProntoCallback = null;

	public static void interromper() {
		preenchimentoEmAndamento = false;
	}

	public static void setOnPreechimentoPronto(
			OnPreechimentoPronto onProntoCallback) {
		Preechimento.onProntoCallback = onProntoCallback;
	}

	/**
	 * Interface para chamada do preenchimento via Boundary-Fill. O
	 * preenchimento eh realizado em outra Thread (@see BoundaryFillThread). A
	 * implementacao do algoritimo encontra-se no metodo
	 * BoundaryFillThread.run()
	 * 
	 * @param bitmap
	 * @param x
	 * @param y
	 * @param corPreenchimento
	 * @param corBorda
	 */
	public static void boundaryFill(Bitmap bitmap, int x, int y,
			int corPreenchimento, int corBorda) {
		if (!preenchimentoEmAndamento) {
			BoundaryFillThread boundaryTh = new BoundaryFillThread(bitmap, x,
					y, corPreenchimento, corBorda);
			boundaryTh.start();
		}
	}

	private static class FloodFillThread extends Thread {
		int x, y, corPreenchimento, corAntiga;
		Bitmap bitmap;
		Handler handler = new Handler();

		FloodFillThread(Bitmap bitmap, int x, int y, int corPreenchimento,
				int corAntiga) {
			this.bitmap = bitmap;
			this.x = x;
			this.y = y;
			this.corPreenchimento = corPreenchimento;
			this.corAntiga = corAntiga;
		}

		/**
		 * Implementacao do algoritmo Flood-Fill (iterativo utilizando pilha).
		 */
		@Override
		public void run() {
			preenchimentoEmAndamento = true;
			Stack<Pair<Integer, Integer>> pilha = new Stack<Pair<Integer, Integer>>();

			pilha.push(new Pair<Integer, Integer>(x, y));
			int bitmapWidth, bitmapHeight;
			synchronized (bitmap) {
				bitmapWidth = bitmap.getWidth();
				bitmapHeight = bitmap.getHeight();
			}
			int corAtual;
			while (!pilha.empty() && preenchimentoEmAndamento) {
				while (!pilha.empty() && pilha.size() < TAMANHO_MAXIMO_PILHA
						&& preenchimentoEmAndamento) {
					Pair<Integer, Integer> topo = pilha.pop();
					x = topo.first;
					y = topo.second;
					if (x >= 0 && y >= 0 && x < bitmapWidth && y < bitmapHeight) {
						synchronized (bitmap) {
							corAtual = bitmap.getPixel(x, y);
						}
						if (corAtual == corAntiga) {
							synchronized (bitmap) {
								bitmap.setPixel(x, y, corPreenchimento);
							}
							// conectividade 4
							pilha.push(new Pair<Integer, Integer>(x + 1, y));
							pilha.push(new Pair<Integer, Integer>(x - 1, y));
							pilha.push(new Pair<Integer, Integer>(x, y + 1));
							pilha.push(new Pair<Integer, Integer>(x, y - 1));
						}
					}
				}
				if (!pilha.empty()) { // ultrapassou TAMANHO_MAXIMO_PILHA
					// esvazia a pilha, passando para outra pilha os pixels
					// que ainda estao na cor antiga
					Stack<Pair<Integer, Integer>> aux = new Stack<Pair<Integer, Integer>>();
					while (!pilha.empty() && preenchimentoEmAndamento) {
						Pair<Integer, Integer> topo = pilha.pop();
						x = topo.first;
						y = topo.second;
						if (x >= 0 && y >= 0 && x < bitmapWidth
								&& y < bitmapHeight) {
							synchronized (bitmap) {
								corAtual = bitmap.getPixel(x, y);
							}
							if (corAtual == corAntiga) {
								aux.insertElementAt(topo, 0);
							}
						}
					}
					pilha = aux;
				}
			}
			preenchimentoEmAndamento = false;
			if (Preechimento.onProntoCallback != null) {
				handler.post(new Runnable() {

					public void run() {
						onProntoCallback.onPronto();
					}
				});

			}
		}
	}

	private static class BoundaryFillThread extends Thread {
		int x, y, corPreenchimento, corBorda;
		Bitmap bitmap;
		Handler handler = new Handler();

		BoundaryFillThread(Bitmap bitmap, int x, int y, int corPreenchimento,
				int corBorda) {
			this.bitmap = bitmap;
			this.x = x;
			this.y = y;
			this.corPreenchimento = corPreenchimento;
			this.corBorda = corBorda;
		}

		/**
		 * Implementacao do algoritmo Boundary-Fill (iterativo utilizando
		 * pilha).
		 */
		@Override
		public void run() {
			preenchimentoEmAndamento = true;
			Stack<Pair<Integer, Integer>> pilha = new Stack<Pair<Integer, Integer>>();
			pilha.push(new Pair<Integer, Integer>(x, y));
			int bitmapWidth, bitmapHeight;
			synchronized (bitmap) {
				bitmapWidth = bitmap.getWidth();
				bitmapHeight = bitmap.getHeight();
			}
			int corAtual;
			while (!pilha.empty() && preenchimentoEmAndamento) {
				while (!pilha.empty() && pilha.size() < TAMANHO_MAXIMO_PILHA
						&& preenchimentoEmAndamento) {
					Pair<Integer, Integer> topo = pilha.pop();
					x = topo.first;
					y = topo.second;
					if (x >= 0 && y >= 0 && x < bitmapWidth && y < bitmapHeight) {
						synchronized (bitmap) {
							corAtual = bitmap.getPixel(x, y);
						}
						if (corAtual != corBorda
								&& corAtual != corPreenchimento) {
							synchronized (bitmap) {
								bitmap.setPixel(x, y, corPreenchimento);
							}
							// conectividade 4
							pilha.push(new Pair<Integer, Integer>(x + 1, y));
							pilha.push(new Pair<Integer, Integer>(x - 1, y));
							pilha.push(new Pair<Integer, Integer>(x, y + 1));
							pilha.push(new Pair<Integer, Integer>(x, y - 1));
						}
					}
				}
				if (!pilha.empty()) { // ultrapassou TAMANHO_MAXIMO_PILHA
					// esvazia a pilha, passando para outra pilha os pixels
					// que ainda estao na cor antiga
					Stack<Pair<Integer, Integer>> aux = new Stack<Pair<Integer, Integer>>();
					while (!pilha.empty() && preenchimentoEmAndamento) {
						Pair<Integer, Integer> topo = pilha.pop();
						x = topo.first;
						y = topo.second;
						if (x >= 0 && y >= 0 && x < bitmapWidth
								&& y < bitmapHeight) {
							synchronized (bitmap) {
								corAtual = bitmap.getPixel(x, y);
							}
							if (corAtual != corPreenchimento) {
								aux.insertElementAt(topo, 0);
							}
						}
					}
					pilha = aux;
				}
			}
			preenchimentoEmAndamento = false;
			if (Preechimento.onProntoCallback != null) {
				handler.post(new Runnable() {

					public void run() {
						onProntoCallback.onPronto();
					}
				});

			}
		}
	}

	public static interface OnPreechimentoPronto {
		public void onPronto();
	}
}